##################################################################
# File : lua/sim/weapon.lua
# Description : The base weapon class for all weapons in the game.
# Author : GPG
##################################################################

local RELOCATING_TIME = TicksPerSecond * 3
local DESIRED_MISS_THRESHOLD = 5
local OPPORTUNITY_MISS_THRESHOLD = 10

local IMPACTEVENT = {
    HitTarget = 0,
    MissTarget = 1,
    Irrelevant = 2, # Projectile/Beam is random so it may or may not hit target anyways
    str = function(self,v) return table.find(self,v) end
}

RANGE = {
    Ok      = 0,
    Near    = 1,
    Side    = 2,
    Far     = 3,
    str = function(self,v) return table.find(self,v) end
}

Weapon = Class(moho.weapon_methods) {
    __init = function(self, unit)
        self.unit = unit
        #LOG('Init weapons relocate timer')
        #self:SetRelocateTimer(0.0)
        #self.RelocateTimer = 0
        self.MissCount = 0
        self.FireCount = 0
        self.MotionState = 'Stopped'
        self.NumAttacks = 0
    end,

    OnCreate = function(self)
        self.Trash = TrashBag()
        self:SetValidTargetsForCurrentLayer(self.unit:GetCurrentLayer())
        if self:GetBlueprint().Turreted == true then
            self:SetupTurret()
        end
        self:SetWeaponPriorities()

        self.Enabled = true

        if self.OnAnimEvent and self.unit.Character then
            for k, state in self.unit.Character.AnimStateControllers do
            	for j, comp in state.Compositions do
					comp.Callbacks:Add(self.OnAnimEvent,self)
			    end
            end
        end
    end,

    OnDestroy = function(self)
        self.Trash:Destroy()
        if self.unit.Character and self.OnAnimEvent then
            self.unit.Character:RemoveAnimEvent(self.OnAnimEvent)
        end
        ChangeState(self,self.DeadState)
    end,

    SetupTurret = function(self)
        local bp = self:GetBlueprint()
        local yawBone = bp.TurretBoneYaw
        local pitchBone = bp.TurretBonePitch
        local muzzleBone = bp.TurretBoneMuzzle
        local precedence = bp.AimControlPrecedence or 10
        local pitchBone2
        local muzzleBone2

        if not self.unit:IsValidBone(yawBone) then
            error('*ERROR: Bone aborting turret setup due to yaw bone issues.', 2)
        end

        if not self.unit:IsValidBone(pitchBone) then
            error('*ERROR: Bone aborting turret setup due to yaw bone issues.', 2)
        end

        if bp.TurretBoneDualPitch and bp.TurretBoneDualPitch != '' then
            pitchBone2 = bp.TurretBoneDualPitch
            if not self.unit:IsValidBone(pitchBone2) then
                error('*ERROR: Bone aborting turret setup due to pitch bone2 issues.', 2)
            end
        end

        if bp.TurretBoneDualMuzzle and bp.TurretBoneDualMuzzle != '' then
            muzzleBone2 = bp.TurretBoneDualMuzzle
            if not self.unit:IsValidBone(muzzleBone2) then
                error('*ERROR: Bone aborting turret setup due to muzzle bone2 issues.', 2)
            end
        end

        if yawBone and pitchBone and muzzleBone then
            if bp.TurretDualManipulators then
                self.AimControl = CreateAimController(self, 'Torso', yawBone)
                self.AimRight = CreateAimController(self, 'Right', pitchBone, pitchBone, muzzleBone)
                self.AimLeft = CreateAimController(self, 'Left', pitchBone2, pitchBone2, muzzleBone2)
                self.AimControl:SetPrecedence(precedence)
                self.AimRight:SetPrecedence(precedence)
                self.AimLeft:SetPrecedence(precedence)
                if EntityCategoryContains(categories.STRUCTURE, self.unit) then
                    self.AimControl:SetResetPoseTime(9999999)
                end
                self:SetFireControl('Right')
                self.unit.TrashOnKilled:Add(self.AimControl)
                self.unit.TrashOnKilled:Add(self.AimRight)
                self.unit.TrashOnKilled:Add(self.AimLeft)
            else
                self.AimControl = CreateAimController(self, 'Default', yawBone, pitchBone, muzzleBone)
                if EntityCategoryContains(categories.STRUCTURE, self.unit) then
                    self.AimControl:SetResetPoseTime(9999999)
                end
                self.unit.Trash:Add(self.AimControl)
                self.AimControl:SetPrecedence(precedence)
                if bp.RackSlavedToTurret and table.getn(bp.RackBones) > 0 then
                    for k, v in bp.RackBones do
                        if v.RackBone != pitchBone then
                            local slaver = CreateSlaver(self.unit, v.RackBone, pitchBone)
                            slaver:SetPrecedence(precedence-1)
                            self.unit.Trash:Add(slaver)
                        end
                    end
                end
            end
        else
            error('*ERROR: Trying to setup a turreted weapon but there are yaw bones, pitch bones or muzzle bones missing from the blueprint.', 2)
        end

        local numbersexist = true
        local turretyawmin, turretyawmax, turretyawspeed
        local turretpitchmin, turretpitchmax, turretpitchspeed

        #SETUP MANIPULATORS AND SET TURRET YAW, PITCH AND SPEED
        if self:GetBlueprint().TurretYaw and self:GetBlueprint().TurretYawRange then
            turretyawmin, turretyawmax = self:GetTurretYawMinMax()
        else
            numbersexist = false
        end
        if self:GetBlueprint().TurretYawSpeed then
            turretyawspeed = self:GetTurretYawSpeed()
        else
            numbersexist = false
        end
        if self:GetBlueprint().TurretPitch and self:GetBlueprint().TurretPitchRange then
            turretpitchmin, turretpitchmax = self:GetTurretPitchMinMax()
        else
            numbersexist = false
        end
        if self:GetBlueprint().TurretPitchSpeed then
            turretpitchspeed = self:GetTurretPitchSpeed()
        else
            numbersexist = false
        end
        if numbersexist then
            self.AimControl:SetFiringArc(turretyawmin, turretyawmax, turretyawspeed, turretpitchmin, turretpitchmax, turretpitchspeed)
            if self.AimRight then
                self.AimRight:SetFiringArc(turretyawmin/12, turretyawmax/12, turretyawspeed, turretpitchmin, turretpitchmax, turretpitchspeed)
            end
            if self.AimLeft then
                self.AimLeft:SetFiringArc(turretyawmin/12, turretyawmax/12, turretyawspeed, turretpitchmin, turretpitchmax, turretpitchspeed)
            end
        else
            local strg = '*ERROR: TRYING TO SETUP A TURRET WITHOUT ALL TURRET NUMBERS IN BLUEPRINT, ABORTING TURRET SETUP. WEAPON: ' .. self:GetBlueprint().Label .. ' UNIT: '.. self.unit:GetUnitId()
            error(strg, 2)
        end
    end,

    AimManipulatorSetEnabled = function(self, enabled)
        if self.AimControl then
            self.AimControl:SetEnabled(enabled)
        end
    end,

    GetAimManipulator = function(self)
        return self.AimControl
    end,

    SetTurretYawSpeed = function(self, speed)
        local turretyawmin, turretyawmax = self:GetTurretYawMinMax()
        local turretpitchmin, turretpitchmax = self:GetTurretPitchMinMax()
        local turretpitchspeed = self:GetTurretPitchSpeed()
        if self.AimControl then
            self.AimControl:SetFiringArc(turretyawmin, turretyawmax, speed, turretpitchmin, turretpitchmax, turretpitchspeed)
        end
    end,

    SetTurretPitchSpeed = function(self, speed)
        local turretyawmin, turretyawmax = self:GetTurretYawMinMax()
        local turretpitchmin, turretpitchmax = self:GetTurretPitchMinMax()
        local turretpitchspeed = self:GetTurretYawSpeed()
        if self.AimControl then
            self.AimControl:SetFiringArc(turretyawmin, turretyawmax, turretyawspeed, turretpitchmin, turretpitchmax, speed)
        end
    end,

    GetTurretYawMinMax = function(self)
        local halfrange = self:GetBlueprint().TurretYawRange
        local yaw = self:GetBlueprint().TurretYaw
        turretyawmin = yaw - halfrange
        turretyawmax = yaw + halfrange
        return turretyawmin, turretyawmax
    end,

    GetTurretYawSpeed = function(self)
        return self:GetBlueprint().TurretYawSpeed
    end,

    GetTurretPitchMinMax = function(self)
        local halfrange = self:GetBlueprint().TurretPitchRange
        local pitch = self:GetBlueprint().TurretPitch
        turretpitchmin = pitch - halfrange
        turretpitchmax = pitch + halfrange
        return turretpitchmin, turretpitchmax
    end,

    GetTurretPitchSpeed = function(self)
        return self:GetBlueprint().TurretPitchSpeed
    end,

    OnFire = function(self)
        self:PlayWeaponSound('OnStartFire')

        # attacks made per target
        self.FireCount = self.FireCount + 1

        # total attacks this weapon has made
        self.NumAttacks = self.NumAttacks + 1
    end,

    OnReload = function(self)
        self:PlayWeaponSound('OnReload')
    end,

    OnEnableWeapon = function(self)
    end,

    OnGotTarget = function(self)
        if self.DisabledFiringBones and self.unit.Animator then
            for key, value in self.DisabledFiringBones do
                self.unit.Animator:SetBoneEnabled(value, false)
            end
        end

        self.FireCount = 0
        self.MissCount = 0
        #LOG(GetGameTick(), ' OnGotTarget')
    end,

    OnLostTarget = function(self)

        if self.DisabledFiringBones and self.unit.Animator then
            for key, value in self.DisabledFiringBones do
                self.unit.Animator:SetBoneEnabled(value, true)
            end
        end
        #LOG(GetGameTick(), ' OnLostTarget')
        self:AbortAttack()
    end,

    OnStartTracking = function(self, label)
    end,

    OnStopTracking = function(self, label)
    end,


    PlayWeaponSound = function(self, sound, loop)
        #LOG( "*DEBUG: Trying to play weapon sound ".. sound)
        local bp = self:GetBlueprint().Audio
        if bp and bp[sound] and bp[sound].Sound then
            #LOG( 'Playing ', sound )
            if not loop then
                self:PlaySound(bp[sound].Sound)
            else
                self:SetAmbientSound(bp[sound].Sound, sound)
            end
            return true
        end
        #LOG( 'Could not play ', sound )
        return false
    end,

    StopWeaponSound = function(self, sound)
        local bp = self:GetBlueprint().Audio
        if bp and bp[sound] and bp[sound].Sound then
            #LOG( 'Playing ', sound )
            self:SetAmbientSound(nil, sound)
            return true
        end
        #LOG( 'Could not stop ', sound )
        return false
    end,

    OnMotionHorzEventChange = function(self, new, old)
        self.MotionState = new
    end,

    GetDamageTable = function(self)
        local bp = self:GetBlueprint()
        local damageTable = {}
        local damage = self:GetBaseDamage()
        if bp.DoNotUseDamageRating then
            damage = bp.Damage or 0
        end
        if bp.DamageRange then
            damage = damage + (Random(0, bp.DamageRange * 2) - bp.DamageRange)
        end
        #LOG("*DEBUG: DAMAGE TO BE DONE: ", repr(damage))

        damageTable.DamageFriendly = bp.DamageFriendly
        if damageTable.DamageFriendly == nil then
            damageTable.DamageFriendly = true
        end
        damageTable.CollideFriendly = bp.CollideFriendly or false
        damageTable.MetaImpactAmount = bp.MetaImpactAmount
        damageTable.MetaImpactRadius = bp.MetaImpactRadius

        #For new damage stuff
        damageTable.Instigator = self.unit
        damageTable.InstigatorArmy = self.unit:GetArmy()
        damageTable.InstigatorBp = self.unit:GetBlueprint()
        damageTable.Amount = damage
        damageTable.Type = bp.DamageType or 'Normal'
        damageTable.DamageAction = bp.Label or 'Unknown'
        damageTable.Origin = self.unit:GetPosition()
        damageTable.Radius = self.ModdedRadius or bp.DamageRadius
        if bp.NoSplashDamage then
            damageTable.Radius = 0
        end
        if bp.CanDamageReturn == nil then
            damageTable.CanDamageReturn = true
        else
            damageTable.CanDamageReturn = bp.CanDamageReturn
        end
        if bp.CanBackfire == nil then
            damageTable.CanBackfire = true
        else
            damageTable.CanBackfire = bp.CanBackfire
        end
        if bp.CanBackfire == nil then
            damageTable.CanCrit = true
        else
            damageTable.CanCrit = bp.CanCrit
        end
        if bp.CanBeEvaded == nil then
            damageTable.CanBeEvaded = true
        else
            damageTable.CanBeEvaded = bp.CanBeEvaded
        end
        damageTable.CanOverKill = bp.CanOverKill
        if damageTable.CanOverKill == nil then
            damageTable.CanOverKill = true
        end

        damageTable.ArmorImmune = bp.ArmorImmune

        damageTable.CanMagicResist = bp.CanMagicResist
        if damageTable.CanMagicResist == nil then
            damageTable.CanMagicResist = true
        end

        damageTable.NoFloatText = bp.NoFloatText

        damageTable.Group = bp.Group or "UNITS"
        return damageTable
    end,

    #we might want to place Get base damage in cpp
    GetBaseDamage = function(self)
        local bp = self:GetBlueprint()
        local damage = (CalculateDamageFromRating(self.unit.Sync.DamageRating)) + (bp.Damage or 0) + (self.DamageBonus or 0)
        return damage
    end,

    CreateProjectileForWeapon = function(self, bone)
        if not self.unit:IsValidBone(bone) then
            WARN("*WARNING: Tried to create projectile at invalid bone "..bone)
            return
        end

        if self.unit.OnWeaponFire then
            self.unit:OnWeaponFire(self)
        end

        local proj = self:CreateProjectile(bone)
        if not proj then
            WARN("*WARNING: Failed to create projectile in CreateProjectileForWeapon")
            return
        end
        local damageTable = self:GetDamageTable()
        if proj and not proj:BeenDestroyed() then
            proj:PassDamageData(self.unit, damageTable)
        end

        self:PlayWeaponSound('OnFireProjectile')
        return proj
    end,

    SetValidTargetsForCurrentLayer = function(self, newLayer)
        #LOG( 'SetValidTargetsForCurrentLayer, layer = ', newLayer )
        local weaponBlueprint = self:GetBlueprint()
        if weaponBlueprint.FireTargetLayerCapsTable then
            if weaponBlueprint.FireTargetLayerCapsTable[newLayer] then
                #LOG( 'Setting Target Layer Caps to ', weaponBlueprint.FireTargetLayerCapsTable[newLayer] )
                self:SetFireTargetLayerCaps( weaponBlueprint.FireTargetLayerCapsTable[newLayer] )
            else
                #LOG( 'Setting Target Layer Caps to None' )
                self:SetFireTargetLayerCaps('None')
            end
        end
    end,

    SetWeaponPriorities = function(self, priTable)
        if not priTable then
            local bp = self:GetBlueprint().TargetPriorities
            if bp then
                local priorityTable = {}
                for k, v in bp do
                    table.insert(priorityTable, ParseEntityCategoryEx(v))
                end
                self:SetTargetingPriorities(priorityTable)
            end
        else
            self:SetTargetingPriorities(priTable)
        end
    end,

    ForkThread = function(self, fn, ...)
        if fn then
            local thread = ForkThread(fn, self, unpack(arg))
            self.unit.TrashOnKilled:Add(thread)
            self.unit.Trash:Add(thread)
            self.Trash:Add(thread)
            return thread
        else
            return nil
        end
    end,

    SetWeaponEnabled = function(self, enable)
        self.Enabled = enable
        self:SetEnabled(enable)
        self:AimManipulatorSetEnabled(enable)
        if enable then
            self:ResumeAcquisition()
            self.unit:OnWeaponEnabled(self)
        else
            self.unit:OnWeaponDisabled(self)
        end
    end,

    IsWeaponEnabled = function(self)
        return self.Enabled
    end,

    SetDamageDelay = function(self, delay)
        self.DamageDelay = delay
    end,

    SetDamageBonus = function(self, bonus)
        self.DamageBonus = bonus
    end,

    SetDamageRadius = function(self, radius)
        self.ModdedRadius = radius
    end,

    OnDoInstaHit = function(self,other)
        local data = self:GetDamageTable()
        DealDamage(data,other)
    end,

    AbortAttack = function(self)
    end,

    DeadState = State {
        Main = function(self)
        end,
    },

    HandleWeaponMiss = function(self)
        local unit = self.unit
        local attacker = self:GetAttacker()
        local desired = attacker:GetDesiredTarget()
        local opportunity = self:GetTarget()
        local realDesired = desired:GetRealTargetEntity()
        local realOpp = opportunity:GetRealTargetEntity()

        if self:IsPrimaryWeapon() and realDesired and unit:IsMobile() and not unit:IsMoving() and not realDesired:IsMoving() then
            # If we are missing a stationary target, we are probably not in a good position, try to move to
            # another better spot and try again.
            self.MissCount = self.MissCount + 1
            if self.MissCount > DESIRED_MISS_THRESHOLD then
                LOG('Relocating time at Handle Weapon miss')
                self:SetRelocateTimer( RELOCATING_TIME )
                #self.SetRelocateTimer = RELOCATING_TIME
            else
                # since we missed, try another aim spot
                DesyncLog('PickNewTargetAimSpot UnitId=' .. unit:GetUnitId())
                self:PickNewTargetAimSpot()
            end
        elseif realOpp and not unit:IsMoving() and not realOpp:IsMoving() then
            # if we keep missing a stationary opportunity target, we will black list it for a period
            # of time and attempt to attack other targets instead.
            self.MissCount = self.MissCount + 1
            if self.MissCount > OPPORTUNITY_MISS_THRESHOLD then
                self:SetTarget(AITarget())
            else
                # since we missed, try another aim spot
                DesyncLog('PickNewTargetAimSpot UnitId=' .. unit:GetUnitId())
                self:PickNewTargetAimSpot()
            end
        end
    end,

    SatisfiedGroundAttackTries = function(self)
        return self.FireCount >= self:GetBlueprint().AttackGroundTries
    end,

    SatisfiedTotalAttacks = function(self)
        local totalAttacks = self:GetBlueprint().TotalAttacks
        if not totalAttacks then
            # if there is no TotalAttacks value, we are never satisfied
            return false
        end

        return self.NumAttacks >= self:GetBlueprint().TotalAttacks
    end,

    OnImpactEvent = function(self,event)
        # ignore weapons that have randomness built in
        if self:GetFiringRandomness() > 0 then
            return
        end

        if event == IMPACTEVENT.HitTarget or event == IMPACTEVENT.Irrelevant then
            self:RelocateTimer( 0 )
            #self.RelocateTimer = 0
            self.MissCount = 0
        elseif event == IMPACTEVENT.MissTarget then
            self:HandleWeaponMiss()
        end
    end,

    CanAutoInitiateAttack = function(self,target)
        if not self:GetBlueprint().AutoInitiateAttackCommand then
            return false
        end

        if self.unit.HoldPosition then
            return false
        end

        local cmd = self.unit:GetCommandQueue():GetCurrentCommand()
        if cmd and cmd:GetType() == 'Attack' then #and cmd:GetTarget():IsTheSameAs(target) then
            return false
        end

        if self.unit:GetCommandQueue():GetNextCommand() then
            return false
        end

        return true
    end,
}
